<?php
/**
* php5 version for class databean.
* This file belongs to a webappkit package (kit)
* @link http://webappkit.net
* @package databeans
* @subpackage databean
* @todo add interface
* @author J.Ducastel <nospam0@ducastel.name>
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
*/

/**
* root class for any data structure : stores, checks and formats data as members.
*
* <p>searches for member-related methods for setting, getting,
* checking, input and output filtering etc of members.</p>
* <p>to allow filters, check etc while accessing members directly, members should be set protected or private</p>
* <p>have automatic methods set{member}, get{member} (using __call)</p>
* <ul>
* <li>bool _{member}_check($x)</li>
* <li>mixed _{member}_get($x)</li>
* <li>mixed _{member}_set($x)</li>
* <li>bool _{member}_gettable($x)</li>
* <li>bool _{member}_settable($x)</li>
* <li>mixed _{member}_inputfilter($x)</li>
* <li>mixed _{member}_outputfilter($x)</li>
* <li>mixed _{member}_autovalue()</li></ul>
* @author J.Ducastel <nospam0@ducastel.name>
*/
class DataBean {
    /**
    * @access public
    * @param array $data associative array members keys => values
    */
    public function __construct($data=NULL) {
        // setting auto values if not provided
        $this->autoValues();
        // setting provided members' values if valid
        if (is_array($data))
            $this->loadMembers($data);
    }/**/
/*------------------------------------------------------------------------------
                                                                  public methods
------------------------------------------------------------------------------*/
    /**
     * sets exceptions/notice throwing, false by default
     * @access public
     * @param bool $newstate won't change if null
     * @return bool new/current state
     */
    public function errors($newstate=null) {
        if (is_bool($newstate))
            $this->_databean_errors=$newstate;
        return $this->_databean_errors;
    }

    /**
    * locks / unlocks databean structure, provides lock status.
    * When structure is locked,
    * properties can't be set if not declared first.
    * @access public
    * @param bool $newstate optional, new lock state
    * @return bool
    */
    public function membersLock($newstate=null) {
        if (is_bool($newstate))
            $this->_databean_members_lock=$newstate;
        return $this->_databean_members_lock;
    }/**/
    /**
    * alias for membersLock()
    */
    public function structureLock($new=null) {
        return $this->membersLock($new);
    }

    /**
    * locks / unlocks databean data, provide lock status.
    * When values are locked, they can't be changed.
    * @access public
    * @param bool $newstate optional, new lock state
    * @return bool
    */
    public function valuesLock($newstate=null) {
        if (!is_null($newstate) and is_bool($newstate))
            $this->_databean_values_lock=$newstate;
        return $this->_databean_values_lock;
    }/**/

    /**
    * Enable/disable inputfilters, provide inputfilters status.
    * when true, members method _{member}_inputfilter($x) will be applied to member new value.
    * @access public
    * @param bool $newstate optional
    * @return bool
    */
    public function inputFilters($newstate=null) {
        if (is_bool($newstate))
            $this->_databean_inputfilters=$newstate;
        return $this->_databean_inputfilters;
    }/**/

    /**
    * enable/disable outputfilters, provide outputfilters status.
    * when true, members method _{member}_outputfilter($x) will be applied to member value's output.
    * @access public
    * @param bool $newstate optional
    * @return bool
    */
    public function outputFilters($newstate=null) {
        if (is_bool($newstate))
            $this->_databean_outputfilters=$newstate;
        return $this->_databean_outputfilters;
    }/**/

    /**
    * enable/disable check before setting members.
    * When true, bool _{member}_check($x) will be called (after input filter) before setting new value.
    * @access public
    * @param bool $newstate optional
    * @return bool
    */
    public function checkOnSet($newstate=null) {
        if (is_bool($newstate))
            $this->_databean_checkonset=$newstate;
        return $this->_databean_checkonset;
    }/**/

    /**
    * Tries to load an associative array as members.
    * @access public
    * @param array $data
    * @return int number of successfully loaded members
    */
    public function loadMembers($data) {
        $loaded=0;
        if ($this->valuesLock() or !is_array($data))
            return 0;
        foreach (array_keys($data) as $key) {
            if ($this->setMember($key,$data[$key])) $loaded++;
        }
        return $loaded;
    }/**/
    /**
    * alias for loadmembers()
    */
    public function loadData($data) {
        return $this->loadMembers($data);
    }/**/

    /**
     * loads data from POST. Avoids magic quotes backslashing
     * @access public
     * @param mixed $data
     * if null, imports $_POST.
     * If string, imports $_POST[$data].
     * If array, imports given data
     * @return int number of successfully loaded members
     */
    public function loadPostData($data=null) {
        switch (true) {
            case is_null($data):
                $data=$_POST;
                break;
            case is_string($data) and isset($_POST[$data]):
                $data=$_POST[$data];
                break;
            default:
                return 0;
        }
        // cleaning backslashes
        if (get_magic_quotes_gpc()) {
            foreach (array_keys($data) as $key) {
                $data[$key]=stripslashes($data[$key]);
            }
        }
        return $this->loadMembers($data);
    }

    /**
     * sets default values for members.
     * imports default values for members
     * and/or applies defaults for null members
     * @access public
     * @param array $data
     * @return int successfully set member's count
     */
    public function loadDefaults($data=array()) {
        if (!is_array($data)) {
            $this->throwError("databean->loaDefaults(array): was not an array");
            return 0;
        }
        $count=0;
        foreach (array_keys($data) as $key) {
            if ($this->setMemberDefault($key,$data[$key]))
                $count++;
        }
        return $count;
    }/**/

    /**
    * Return a member's value.
    * <ol>
    * <li>checks memberGettable(), may abort if false</li>
    * <li>returns _{member}_get() if method exists</li>
    * <li>gets current value or null if member don't exist</li>
    * <li>returns memberOutputFilter()</li>
    * </ol>
    * Output filter method _{member}_outputfilter($x) may be applied before.
    * @param string $key
    * @access public
    * @return mixed
    */
    public function getMember($key) {
        // checking get is allowed
        if (!$this->memberGettable($key)) {
            $this->_throwError("databean->get() not allowed for $key");
            return null;
        }
        // searching for custom method
        $method='_'.$key.'_get';
        if (method_exists($this,$method)) {
            return $this->$method();
        }
        // getting member's value
        $value=isset($this->$key)?$this->$key:null;
        // output filtering
        if ($this->outputFilters())
            return $this->memberOutputFilter($key,$value);
        return $value;
    }/**/
    /**
    * alias for getMember()
    */
    public function get($key) {
        return $this->getMember($key);
    }
    /**
     * magic getter
     * intercepts calls to members IF they are not public
     * and forward them to getMember
     * @param string $key member name
     * @return mixed
     */
    public function __get($key) {
        if (substr($key,0,1)!='_')
            return $this->getMember($key);
        return $this->$key;
    }

    /**
    * lists members. return members key/name in array
    * @access public
    * @return array
    */
    public function listMembers() {
        return $this->_getMembers();
    }/**/

    /**
     * lists required members
     * @access public
     * @return array
     */
    public function listRequiredMembers() {
        $all=$this->listMembers();
        $required=array();
        foreach ($all as $key) {
            $method='_'.$key.'_required';
            if (method_exists($this,$method) and $this->$method())
                $required[]=$key;
        }
        return $required;
    }/**/

    /**
    * Indicates wether member is gettable.
    * search for custom bool _{key}_gettable() method
    * then checks if member exists or have a _{key}_set() generator
    * @access public
    * @param string $key
    * @return bool
    */
    public function memberGettable($key) {
        // searching for member's custom method
        $method='_'.$key.'_gettable';
        if (method_exists($this,$method)) {
            return $this->$method();
        }
        // searching for member or generator
        $method='_'.$key.'_get';
        if (!$this->hasMember($key) and !method_exists($this,$method))
            return false;
        return true;
    }/**/

    /**
    * Get all members as associative array.
    * @access public
    * @return array
    */
    public function & getAllAssoc() {
        $assoc=array();
        $keys=$this->_databean_getMembers();
        foreach ($keys as $key)
            $assoc[$key]=$this->getMember($key);
        return $assoc;
    }/**/
    /**
    * alias for getallassoc()
    */
    public function & getAll() {
        $assoc=&$this->getAllAssoc();
        return $assoc;
    }/**/

    /**
    * returns an html representation
    * @access public
    * @return string
    * /
    function toHtml() {
        $data=$this->getAllAssoc(); //print_r($data);
        $html='<dl>';
        foreach ($data as $key=>$value) {
            $html.='<dt>'.$key.'</dt>'
            .'<dd>'.gettype($value).' '.strval($value).'</dd>';
        }
        $html.='</dl>';
        return $html;
    }/**/

    /**
    * Set databean member's value, if checks are ok.
    * <ol>
    * <li>checks for membersLock(), may abort if true and member don't exists</li>
    * <li>checks for memberSettable(), may abort if false</li>
    * <li>search for custom setting method _{member}_set(). if found, return its result</li>
    * <li>applies _{member}_inputfilter() if exists</li>
    * <li>calls checkMember(), may abort if false</li>
    * <li>finally sets new value</li>
    * </ol>
    * @param string $key
    * @param mixed $value
    * @access public
    * @return bool
    */
    public function setMember($key,$value) {
        // checking values lock
        if ($this->valuesLock()) {
            $this->_throwError("databean->set(): values are locked");
            return false;
        }
        // checking set allowed
        if (!$this->memberSettable($key)) {
            $this->_throwError("databean->set() not allowed for $key");
            return false;
        }
        // searching for custom method
        $method='_'.$key.'_set';
        if (method_exists($this,$method)) {
            return $this->$method($value);
        }
        // applying input filter
        if ($this->inputFilters()) {
            $value=&$this->memberInputFilter($key,$value);
        }
        // checking validity
        if ($this->checkOnSet() and !$this->memberCheck($key,$value)) {
            $this->_throwError(get_class($this)."->setMember($key,$value) : value doesn't pass check");
            return false;
        }
        // finally setting value
        $this->$key=$value;
        return true;
    }/**/
    /**
    * alias for setMember()
    */
    public function set($key,$value) {
        return $this->setMember($key,$value);
    }
    /**
     * magic setter
     * intercepts members affectation IF they are not public
     * and forward them to setMember
     * @param string $key
     * @param mixed $value
     * @return bool
     */
    public function __set($key,$value) {
        if (substr($key,0,1)!='_')
            return $this->setMember($key,$value);
        return $this->$key;
    }

    /**
     * set's member default : sets member only if null.
     * @access public
     * @param string $key
     * @param mixed $value
     * @return bool
     */
    public function setMemberDefault($key,$value) {
        if (is_null($this->$key))
            return $this->setMember($key,$value);
        else
            return false;
    }/**/

    /**
    * Indicates wether set is allowed for member.
    * <ol>
    * <li>search for _{member}_settable()</li>
    * <li>checks structureLock(). If true, member have to be declared before.</li>
    *</ol>
    * @access public
    * @param string $key member's name
    * @return bool
    */
    public function memberSettable($key) {
        // searching for member's custom method
        $method='_'.$key.'_settable';
        if (method_exists($this,$method)) {
            return $this->$method();
        }
        // disallow if values are locked
        if ($this->valuesLock()) {
            return false;
        }
        // disallow if structure is locked and is not declared property
        if ($this->membersLock() and !$this->hasMember($key)) {
            $this->_throwError($key.' is not a member of '.get_class($this).' and structure is locked');
            return false;
        }
        return true;
    }

    /**
     * tells if bean has a member with this name
     * @access public
     * @param string $name
     * @return bool
     */
    public function hasMember($name) {
        return in_array($name,$this->listMembers());
    }
    /*public function __isset($key) {
        return $this->hasMember($key);
    }*/

    /**
    * Search for member's input filter and applies it to return value,  or return value.
    * input filter methods : _{member}_inputfilter($value).
    * @access public
    * @param string $key
    * @param mixed $value
    * @return mixed
    */
    public function & memberInputFilter($key,$value) {
        $this->_databean_callCustomMethod("_{$key}_inputfilter",$value);
        return $value;
    }/**/

    /**
    * search for member's input filter and applies it on value, or return value
    * output filter methods : _{member}_outputfilter($value).
    * @access public
    * @param string $key
    * @param mixed $value
    * @return mixed
    */
    public function & memberOutputFilter($key,$value) {
        $this->_databean_callCustomMethod("_{$key}_outputfilter",$value);
        return $value;
    }/**/

    /**
    * Checks a potential value for a member
    * @access public
    * @param string $key
    * @param mixed $value
    * @return bool
    */
    public function memberCheck($key, $value) {
        // custom checking methods
        $method="_{$key}_check";
        if (method_exists($this,$method)) {
            return $this->$method($value);
        }
        // no check defined
        return true;
    }/**/

    /**
    * checks bean global validity.
    * Actually checks every member flagged as required :
    * _{member}_required() returning true
    * @access public
    * @return bool
    */
    public function globalCheck() {
        return $this->_checkRequiredMembers();
    }

    /**
    * member's validity, or calls glocbalCheck if member name's not provided
    * @access public
    * @param string $member optional, member name
    * @return bool
    */
    public function isValid($member=null) {
        if (is_null($member))
            return $this->globalCheck();
        else
            return $this->memberCheck($member,$this->get($member));
    }/**/

    /**
    * Assign automatic values for fields.
    * searches for _{member}_autovalue() methods
    * @access public
    * @return int assigned values count
    */
    public function autoValues() {
        $members=$this->_databean_getMembers();
        $count = 0;
        foreach ($members as $key) {
            $method="_{$key}_autovalue";
            $property=$method;
            if (method_exists($this,$method)) {
                $this->$key=$this->$method();
                $count++;
            }/* else if (isset($this->$property)) {
                $this->$key=$this->$property;
            }*/
        }
        return $count;
    }/**/

    /**
     * redefining isset()
     */
    public function __isset($key) {
        return in_array($key,$this->listMembers()) and !is_null($this->$key);
    }

    /**
     * add standard bean method such as get{member}, set{member} etc
     * @param string $method
     * @param array $args
     * @return mixed
     */
    public function __call($method, $args=array()) {
        $method=strtolower($method);
        // get{member}
        if (substr($method,0,3)=='get') {
            $member=substr($method,3);
            return $this->getMember($member);
        } else if (substr($method,0,3)=='set') {
            $member=substr($method,3);
            return $this->setMember($member,$args[0]);
        }
    }

/*------------------------------------------------------------------------------
                                                                      properties
------------------------------------------------------------------------------*/
    /**
    * @var bool enable/disable set() and loadData().
    * Changed by valuesLock()
    */
    private $_databean_values_lock=false;
    /**
    * @var bool enable/disable setting undeclared members
    * changed by membersLock()
    */
    private $_databean_members_lock=false;
    /**
    * @var bool enable/disable input filter methods on setMember()
    * changed by inputFilters()
    */
    private $_databean_inputfilters=true;
    /**
    * @var bool enable/disable output filter methods on getMember()
    * changed by outputFilters()
    */
    private $_databean_outputfilters=true;
    /**
    * @var bool if true, check member's validity before setting and don't set if invalid
    * changed by checkOnSet()
    */
    private $_databean_checkonset=true;

    /**
     * @var bool will trigger errors/exceptions if true
     */
    private $_databean_errors=false;
    /**
    * @var bool sets _databean_lock_members=true after building
    */
    //var $_databean_lock_members_afterbuilt=false;
    /**
    * @var bool sets _databean_lock_structure=true after building
    */
    //var $_databean_lock_structure_afterbuilt=false;
/*------------------------------------------------------------------------------
                                                               protected methods
------------------------------------------------------------------------------*/

    /**
    * return bean's declared (public) members
    * i.e. all vars not starting by an underscore
    * @access protected
    * @return array
    */
    private function & _databean_getMembers() {
        // getting all instance vars
        //$class=get_class($this);
        $allvars=array_keys(get_object_vars($this));
        $members=array();
        foreach ($allvars as $var) {
            if (substr($var,0,1)!='_')
                $members[]=$var;
        }
        return $members;
    }/**/
    /**
    * alias for _databean_getMembers()
    */
    protected function _getMembers() {
        return $this->_databean_getMembers();
    }

    /**
     *
     */


    /**
    * Checks validity for all members flagged as required
    * @access protected
    * @return bool
    */
    private function _databean_checkRequiredMembers() {
        $members=$this->_databean_getMembers();
        foreach ($members as $key) {
            $method='_'.$key.'_required';
            if (method_exists($this,$method)
            and $this->$method()
            and !$this->memberCheck($key,$this->$key))
                return false;
        }
        return true;
    }/**/
    /**
    * alias for _databean_checkrequiredmembers()
    */
    protected function _checkRequiredMembers() {
        return $this->_databean_checkRequiredMembers();
    }/**/

    /**
    * Applies custom own method on value.
    * works by reference !
    * @param string $method
    * @param mixed &$value
    * @access protected
    * @return bool
    */
    protected function _databean_callCustomMethod($method,&$value) {
        if (!method_exists($this,$method))
            return false;
        $value=$this->$method($value);
        return true;
    }/**/

    protected function _throwError($msg=null) {
        if ($this->errors()) {
            // trigger_error($msg,E_USER_NOTICE);
            throw new Exception($msg);
        }
    }

/*------------------------------------------------------------------------------
 conversion methods to be used in filters
------------------------------------------------------------------------------*/

    protected function _toBool($x) {
        return $x?true:false;
    }

    /**
    * value to datetimebean
    * @param mixed $value
    * @access private
    * @return datetimebean
    */
    protected function & _toDateTimeBean($value) {
        if (class_exists('datetimebean')
        and !is_null($value)
        and $value
        and !is_a($value, 'DateTimeBean'))
            $value = new DateTimeBean($value);
        return $value;
    }/**/

    /**
    * converts to integer if string made of digits
    * @param mixed $value
    * @access private
    * @return int
    */
    protected function & _toInteger($value) {
        if (is_int($value))
            return $value;
        else if (is_string($value) and $value
        and strval(intval($value))==$value)
            $value=intval($value);
        else $value=null;
        return $value;
    }/**/
}
